import os
import hashlib
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
import threading
from collections import defaultdict
import time

class DuplicateFileFinder:
    def __init__(self, root):
        self.root = root
        self.root.title("重复文件查找工具")
        self.root.geometry("800x600")
        self.root.minsize(800, 600)
        
        # 存储重复文件的字典
        self.duplicate_files = {}
        
        # 创建界面
        self.create_widgets()
        
        # 标记是否正在扫描
        self.scanning = False
        
        # 扫描统计
        self.scanned_files = 0
        self.scanned_size = 0
        self.start_time = 0
        
    def create_widgets(self):
        # 创建主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # 目录选择框架
        dir_frame = ttk.LabelFrame(main_frame, text="选择目录", padding="10")
        dir_frame.pack(fill=tk.X, pady=5)
        
        self.dir_entry = ttk.Entry(dir_frame, width=70)
        self.dir_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
        
        browse_button = ttk.Button(dir_frame, text="浏览...", command=self.browse_directory)
        browse_button.pack(side=tk.LEFT, padx=5)
        
        # 过滤选项框架
        filter_frame = ttk.LabelFrame(main_frame, text="过滤选项", padding="10")
        filter_frame.pack(fill=tk.X, pady=5)
        
        # 最小文件大小
        ttk.Label(filter_frame, text="最小文件大小:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        self.min_size_var = tk.StringVar(value="0")
        self.min_size_entry = ttk.Entry(filter_frame, width=10, textvariable=self.min_size_var)
        self.min_size_entry.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W)
        
        self.min_size_unit = ttk.Combobox(filter_frame, values=["B", "KB", "MB", "GB"], width=5)
        self.min_size_unit.current(1)  # 默认选择KB
        self.min_size_unit.grid(row=0, column=2, padx=5, pady=5, sticky=tk.W)
        
        # 最大文件大小
        ttk.Label(filter_frame, text="最大文件大小:").grid(row=0, column=3, padx=5, pady=5, sticky=tk.W)
        self.max_size_var = tk.StringVar(value="")
        self.max_size_entry = ttk.Entry(filter_frame, width=10, textvariable=self.max_size_var)
        self.max_size_entry.grid(row=0, column=4, padx=5, pady=5, sticky=tk.W)
        
        self.max_size_unit = ttk.Combobox(filter_frame, values=["B", "KB", "MB", "GB"], width=5)
        self.max_size_unit.current(2)  # 默认选择MB
        self.max_size_unit.grid(row=0, column=5, padx=5, pady=5, sticky=tk.W)
        
        # 文件类型过滤
        ttk.Label(filter_frame, text="文件类型 (用逗号分隔):").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
        self.file_types_var = tk.StringVar(value="")
        self.file_types_entry = ttk.Entry(filter_frame, width=40, textvariable=self.file_types_var)
        self.file_types_entry.grid(row=1, column=1, columnspan=5, padx=5, pady=5, sticky=tk.W+tk.E)
        ttk.Label(filter_frame, text="例如: jpg,png,mp4").grid(row=1, column=6, padx=5, pady=5, sticky=tk.W)
        
        # 扫描按钮
        scan_frame = ttk.Frame(main_frame)
        scan_frame.pack(fill=tk.X, pady=5)
        
        self.scan_button = ttk.Button(scan_frame, text="开始扫描", command=self.start_scan)
        self.scan_button.pack(side=tk.LEFT, padx=5)
        
        self.progress_bar = ttk.Progressbar(scan_frame, mode="determinate", length=200)
        self.progress_bar.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
        
        self.status_label = ttk.Label(scan_frame, text="就绪")
        self.status_label.pack(side=tk.LEFT, padx=5)
        
        # 结果框架
        result_frame = ttk.LabelFrame(main_frame, text="重复文件", padding="10")
        result_frame.pack(fill=tk.BOTH, expand=True, pady=5)
        
        # 创建树状视图
        self.tree = ttk.Treeview(result_frame, columns=("size", "path"), show="tree headings")
        self.tree.heading("#0", text="文件组")
        self.tree.heading("size", text="大小")
        self.tree.heading("path", text="路径")
        self.tree.column("#0", width=150)
        self.tree.column("size", width=100)
        self.tree.column("path", width=500)
        
        # 绑定双击事件
        self.tree.bind("<Double-1>", self.on_tree_double_click)
        
        # 添加滚动条
        scrollbar_y = ttk.Scrollbar(result_frame, orient="vertical", command=self.tree.yview)
        self.tree.configure(yscrollcommand=scrollbar_y.set)
        
        scrollbar_x = ttk.Scrollbar(result_frame, orient="horizontal", command=self.tree.xview)
        self.tree.configure(xscrollcommand=scrollbar_x.set)
        
        self.tree.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
        scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y)
        scrollbar_x.pack(side=tk.BOTTOM, fill=tk.X)
        
        # 操作按钮框架
        button_frame = ttk.Frame(main_frame)
        button_frame.pack(fill=tk.X, pady=5)
        
        self.delete_button = ttk.Button(button_frame, text="删除选中文件", command=self.delete_selected_files)
        self.delete_button.pack(side=tk.RIGHT, padx=5)
        
        self.select_all_button = ttk.Button(button_frame, text="全选", command=self.select_all_files)
        self.select_all_button.pack(side=tk.RIGHT, padx=5)
        
        self.select_duplicates_button = ttk.Button(button_frame, text="选择重复项", command=self.select_duplicates)
        self.select_duplicates_button.pack(side=tk.RIGHT, padx=5)
        
        # 初始状态
        self.delete_button.config(state=tk.DISABLED)
        self.select_all_button.config(state=tk.DISABLED)
        self.select_duplicates_button.config(state=tk.DISABLED)
    
    def browse_directory(self):
        """打开文件对话框选择目录"""
        directory = filedialog.askdirectory()
        if directory:
            self.dir_entry.delete(0, tk.END)
            self.dir_entry.insert(0, directory)
    
    def start_scan(self):
        """开始扫描重复文件"""
        directory = self.dir_entry.get().strip()
        if not directory:
            messagebox.showerror("错误", "请选择一个目录")
            return
        
        if not os.path.isdir(directory):
            messagebox.showerror("错误", "所选路径不是一个有效的目录")
            return
        
        # 如果已经在扫描，则停止
        if self.scanning:
            self.scanning = False
            self.scan_button.config(text="开始扫描")
            self.status_label.config(text="扫描已停止")
            self.progress_bar.stop()
            return
        
        # 获取过滤条件
        try:
            min_size = self.get_size_in_bytes(self.min_size_var.get(), self.min_size_unit.get())
            max_size = self.get_size_in_bytes(self.max_size_var.get(), self.max_size_unit.get()) if self.max_size_var.get() else float('inf')
            
            if min_size > max_size and max_size != float('inf'):
                messagebox.showerror("错误", "最小文件大小不能大于最大文件大小")
                return
            
            file_types = [ext.strip().lower() for ext in self.file_types_var.get().split(',') if ext.strip()]
        except ValueError:
            messagebox.showerror("错误", "请输入有效的文件大小")
            return
        
        # 清空树状视图
        for item in self.tree.get_children():
            self.tree.delete(item)
        
        # 重置扫描统计
        self.scanned_files = 0
        self.scanned_size = 0
        self.start_time = time.time()
        
        # 更新界面状态
        self.scanning = True
        self.scan_button.config(text="停止扫描")
        self.status_label.config(text="正在扫描...")
        self.progress_bar["value"] = 0
        self.delete_button.config(state=tk.DISABLED)
        self.select_all_button.config(state=tk.DISABLED)
        self.select_duplicates_button.config(state=tk.DISABLED)
        
        # 在新线程中执行扫描
        threading.Thread(
            target=self.scan_directory, 
            args=(directory, min_size, max_size, file_types), 
            daemon=True
        ).start()
    
    def get_size_in_bytes(self, size_str, unit):
        """将大小转换为字节"""
        if not size_str:
            return 0
        
        size = float(size_str)
        if unit == "KB":
            return size * 1024
        elif unit == "MB":
            return size * 1024 * 1024
        elif unit == "GB":
            return size * 1024 * 1024 * 1024
        return size
    
    def scan_directory(self, directory, min_size, max_size, file_types):
        """扫描目录查找重复文件"""
        try:
            # 按大小分组文件
            size_dict = defaultdict(list)
            
            # 获取目录中的所有文件
            all_files = []
            for root, _, files in os.walk(directory):
                for file in files:
                    file_path = os.path.join(root, file)
                    all_files.append(file_path)
            
            total_files = len(all_files)
            
            # 遍历文件
            for i, file_path in enumerate(all_files):
                if not self.scanning:  # 检查是否停止扫描
                    break
                
                try:
                    # 检查文件类型
                    if file_types and not any(file_path.lower().endswith(f".{ext}") for ext in file_types):
                        continue
                    
                    # 检查文件大小
                    file_size = os.path.getsize(file_path)
                    if min_size <= file_size <= max_size:
                        size_dict[file_size].append(file_path)
                    
                    # 更新扫描统计
                    self.scanned_files += 1
                    self.scanned_size += file_size
                    
                    # 更新进度条
                    progress = (i + 1) / total_files * 100
                    self.root.after(0, lambda p=progress: self.update_progress(p))
                    
                    # 每100个文件更新一次状态
                    if i % 100 == 0:
                        self.root.after(0, self.update_status)
                        
                except (OSError, PermissionError):
                    continue
            
            # 查找重复文件
            duplicate_files = {}
            group_id = 1
            
            # 更新状态
            self.root.after(0, lambda: self.status_label.config(text="正在计算文件哈希..."))
            
            # 计算文件哈希
            for size, files in size_dict.items():
                if not self.scanning:  # 检查是否停止扫描
                    break
                
                if len(files) > 1:  # 只处理有多个文件的大小组
                    hash_dict = defaultdict(list)
                    
                    for file_path in files:
                        if not self.scanning:  # 检查是否停止扫描
                            break
                        
                        try:
                            file_hash = self.get_file_hash(file_path)
                            hash_dict[file_hash].append(file_path)
                        except (OSError, PermissionError):
                            continue
                    
                    # 添加重复文件
                    for file_hash, file_paths in hash_dict.items():
                        if len(file_paths) > 1:  # 只添加有重复的文件
                            duplicate_files[f"Group {group_id}"] = {
                                "size": size,
                                "files": file_paths
                            }
                            group_id += 1
            
            # 保存结果
            self.duplicate_files = duplicate_files
            
            # 更新界面
            self.root.after(0, self.update_results)
        
        finally:
            # 恢复界面状态
            self.root.after(0, self.finish_scan)
    
    def update_progress(self, progress):
        """更新进度条"""
        self.progress_bar["value"] = progress
    
    def update_status(self):
        """更新状态标签"""
        elapsed_time = time.time() - self.start_time
        if elapsed_time > 0:
            files_per_second = self.scanned_files / elapsed_time
            size_per_second = self.scanned_size / elapsed_time
            
            status = f"已扫描: {self.scanned_files} 文件, {self.format_size(self.scanned_size)} "
            status += f"({self.format_size(size_per_second)}/s)"
            
            self.status_label.config(text=status)
    
    def get_file_hash(self, file_path, block_size=65536):
        """计算文件的MD5哈希值"""
        hasher = hashlib.md5()
        with open(file_path, 'rb') as file:
            buf = file.read(block_size)
            while len(buf) > 0:
                hasher.update(buf)
                buf = file.read(block_size)
        return hasher.hexdigest()
    
    def update_results(self):
        """更新结果到树状视图"""
        # 清空树状视图
        for item in self.tree.get_children():
            self.tree.delete(item)
        
        # 添加重复文件组
        for group_name, group_info in self.duplicate_files.items():
            size = group_info["size"]
            files = group_info["files"]
            
            # 格式化文件大小
            size_str = self.format_size(size)
            
            # 添加组节点
            group_node = self.tree.insert("", "end", text=group_name, values=(size_str, f"{len(files)}个重复文件"))
            
            # 添加文件节点
            for file_path in files:
                self.tree.insert(group_node, "end", text="", values=("", file_path), tags=("file",))
        
        # 启用按钮
        if self.duplicate_files:
            self.delete_button.config(state=tk.NORMAL)
            self.select_all_button.config(state=tk.NORMAL)
            self.select_duplicates_button.config(state=tk.NORMAL)
    
    def format_size(self, size_bytes):
        """格式化文件大小"""
        for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
            if size_bytes < 1024.0:
                return f"{size_bytes:.2f} {unit}"
            size_bytes /= 1024.0
        return f"{size_bytes:.2f} PB"
    
    def finish_scan(self):
        """完成扫描"""
        self.scanning = False
        self.scan_button.config(text="开始扫描")
        self.progress_bar["value"] = 100
        
        if self.duplicate_files:
            # 计算总重复文件数和总大小
            total_files = sum(len(group_info["files"]) for group_info in self.duplicate_files.values())
            total_size = sum(group_info["size"] * len(group_info["files"]) for group_info in self.duplicate_files.values())
            wasted_size = sum(group_info["size"] * (len(group_info["files"]) - 1) for group_info in self.duplicate_files.values())
            
            self.status_label.config(
                text=f"找到 {len(self.duplicate_files)} 组重复文件，共 {total_files} 个文件 ({self.format_size(total_size)})，"
                f"浪费空间 {self.format_size(wasted_size)}"
            )
        else:
            self.status_label.config(text="未找到重复文件")
    
    def select_all_files(self):
        """选择所有文件"""
        # 取消当前选择
        self.tree.selection_remove(self.tree.selection())
        
        # 选择所有文件节点
        for group_item in self.tree.get_children():
            for file_item in self.tree.get_children(group_item):
                self.tree.selection_add(file_item)
    
    def select_duplicates(self):
        """智能选择重复文件，保留每组的第一个文件"""
        # 取消当前选择
        self.tree.selection_remove(self.tree.selection())
        
        # 选择每组中除第一个外的所有文件
        for group_item in self.tree.get_children():
            children = self.tree.get_children(group_item)
            if len(children) > 1:
                for file_item in children[1:]:  # 跳过第一个文件
                    self.tree.selection_add(file_item)
    
    def delete_selected_files(self):
        """删除选中的文件"""
        selected_items = self.tree.selection()
        if not selected_items:
            messagebox.showinfo("提示", "请选择要删除的文件")
            return
        
        # 收集要删除的文件
        files_to_delete = []
        for item in selected_items:
            # 检查是否是文件节点
            if "file" in self.tree.item(item, "tags"):
                file_path = self.tree.item(item, "values")[1]
                files_to_delete.append(file_path)
        
        if not files_to_delete:
            messagebox.showinfo("提示", "请选择要删除的文件（不是文件组）")
            return
        
        # 确认删除
        confirm = messagebox.askyesno("确认删除", f"确定要删除选中的 {len(files_to_delete)} 个文件吗？\n此操作不可撤销！")
        if not confirm:
            return
        
        # 执行删除
        deleted_count = 0
        failed_files = []
        
        for file_path in files_to_delete:
            try:
                os.remove(file_path)
                deleted_count += 1
                
                # 从树状视图中移除
                for item in self.tree.get_children():
                    for child in self.tree.get_children(item):
                        if self.tree.item(child, "values")[1] == file_path:
                            self.tree.delete(child)
                            break
            except (OSError, PermissionError) as e:
                failed_files.append((file_path, str(e)))
        
        # 检查并删除空组
        for item in self.tree.get_children():
            if not self.tree.get_children(item):
                self.tree.delete(item)
        
        # 显示结果
        if failed_files:
            error_message = "以下文件删除失败：\n"
            for file_path, error in failed_files[:5]:  # 只显示前5个
                error_message += f"{os.path.basename(file_path)}: {error}\n"
            if len(failed_files) > 5:
                error_message += f"...以及其他 {len(failed_files) - 5} 个文件"
            messagebox.showerror("删除失败", error_message)
        
        messagebox.showinfo("删除完成", f"成功删除 {deleted_count} 个文件，失败 {len(failed_files)} 个文件")
    
    def on_tree_double_click(self, event):
        """处理树状视图的双击事件，打开选中的文件"""
        # 获取点击的项目
        item = self.tree.identify('item', event.x, event.y)
        if not item:
            return
            
        # 检查是否是文件节点（不是组节点）
        if "file" in self.tree.item(item, "tags"):
            # 获取文件路径
            file_path = self.tree.item(item, "values")[1]
            
            # 检查文件是否存在
            if os.path.exists(file_path):
                try:
                    # 在Windows上使用os.startfile打开文件
                    os.startfile(file_path)
                except Exception as e:
                    messagebox.showerror("错误", f"无法打开文件: {e}")
            else:
                messagebox.showerror("错误", f"文件不存在: {file_path}")

if __name__ == "__main__":
    root = tk.Tk()
    app = DuplicateFileFinder(root)
    root.mainloop()